package com.nervenets.general.web.params;

import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.collect.Lists;
import com.nervenets.general.annotation.IgnoreSwaggerParameter;
import com.nervenets.general.utils.ClassUtils;
import com.nervenets.general.utils.GeoUtil;
import com.nervenets.general.utils.SpringContextHolder;
import com.nervenets.general.utils.StringUtils;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.hibernate.query.criteria.internal.OrderImpl;
import org.springframework.data.domain.Sort;

import javax.persistence.Tuple;
import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static org.apache.commons.lang3.StringUtils.trim;

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
public abstract class QueryParams<T> implements Params {
    @IgnoreSwaggerParameter
    @JsonIgnore
    @JSONField(serialize = false)
    protected List<Predicate> predicates;
    @IgnoreSwaggerParameter
    @JsonIgnore
    @JSONField(serialize = false)
    protected Root<?> root;
    @IgnoreSwaggerParameter
    @JsonIgnore
    @JSONField(serialize = false)
    protected CriteriaQuery<?> query;
    @IgnoreSwaggerParameter
    @JsonIgnore
    @JSONField(serialize = false)
    protected CriteriaBuilder builder;

    public abstract void generateSpecification(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder);

    @SneakyThrows
    public void generateCustomSpecification(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        this.predicates = predicates;
        this.root = root;
        this.query = query;
        this.builder = builder;
        ClassUtils.trimAllStringValue(this);
        this.generateSpecification(predicates, root, query, builder);
    }

    public void addEqualsPredicate(Expression<?> exp, Object var) {
        this.addEqualsPredicate(exp, var, true);
    }

    public void addEqualsPredicate(Expression<?> exp, Object var, boolean ok) {
        if (null != var && ok) {
            predicates.add(builder.equal(exp, var));
        }
    }

    public void addNotEqualsPredicate(Expression<?> exp, Object var) {
        this.addNotEqualsPredicate(exp, var, true);
    }

    public void addNotEqualsPredicate(Expression<?> exp, Object var, boolean ok) {
        if (null != var && ok) {
            predicates.add(builder.notEqual(exp, var));
        }
    }

    public void addYesOrNoPredicate(Expression<Boolean> exp, Object var, Object yes, Object no) {
        if (null != var) {
            if (var.equals(yes)) {
                predicates.add(builder.isTrue(exp));
            } else if (var.equals(no)) {
                predicates.add(builder.isFalse(exp));
            }
        }
    }

    public void addBetweenPredicate(Expression<? extends Long> exp, long start, long end) {
        if (start > 0 && end > 0) {
            predicates.add(builder.between(exp, start, end));
        } else if (start > 0) {
            addGreaterThanPredicate(exp, start, true, true);
        } else if (end > 0) {
            addLessThanPredicate(exp, end, true, true);
        }
    }

    public <Y extends Comparable<? super Y>> void addBetweenPredicate(Expression<? extends Y> exp, Y start, Y end) {
        if (!Objects.isNull(start) && !Objects.isNull(end)) {
            predicates.add(builder.between(exp, start, end));
        } else if (!Objects.isNull(start)) {
            addGreaterThanPredicate(exp, start, true, true);
        } else if (!Objects.isNull(end)) {
            addLessThanPredicate(exp, end, true, true);
        }
    }

    public <Y extends Comparable<? super Y>> void addGreaterThanPredicate(Expression<? extends Y> exp, Y var, boolean equals) {
        this.addGreaterThanPredicate(exp, var, equals, true);
    }

    public <Y extends Comparable<? super Y>> void addGreaterThanPredicate(Expression<? extends Y> exp, Y var, boolean equals, boolean ok) {
        if (ok) {
            predicates.add(equals ? builder.greaterThanOrEqualTo(exp, var) : builder.greaterThan(exp, var));
        }
    }

    public <Y extends Comparable<? super Y>> void addLessThanPredicate(Expression<? extends Y> exp, Y var, boolean equals) {
        this.addLessThanPredicate(exp, var, equals, true);
    }

    public <Y extends Comparable<? super Y>> void addLessThanPredicate(Expression<? extends Y> exp, Y var, boolean equals, boolean ok) {
        if (ok) {
            predicates.add(equals ? builder.lessThanOrEqualTo(exp, var) : builder.lessThan(exp, var));
        }
    }

    public void addInPredicate(Expression<?> exp, List<?> values) {
        if (null != values && !values.isEmpty()) {
            if (values.size() == 1) {
                addEqualsPredicate(exp, values.get(0), true);
            } else {
                final CriteriaBuilder.In<Object> in = builder.in(exp);
                values.forEach(in::value);
                predicates.add(in);
            }
        }
    }

    public void addNotInPredicate(Expression<?> exp, List<?> values) {
        if (null != values && !values.isEmpty()) {
            if (values.size() == 1) {
                addNotEqualsPredicate(exp, values.get(0), true);
            } else {
                final CriteriaBuilder.In<Object> in = builder.in(exp);
                values.forEach(in::value);
                predicates.add(builder.not(in));
            }
        }
    }

    @SafeVarargs
    public final void addOrLikePredicate(String keyword, Expression<String>... exps) {
        if (null == exps || exps.length == 0 || StringUtils.isBlank(keyword)) return;
        ArrayList<Expression<String>> expressions = Lists.newArrayList(exps);

        if (expressions.size() == 1) {
            predicates.add(builder.like(expressions.get(0), "%" + trim(keyword) + "%"));
        } else {
            Predicate[] pres = new Predicate[expressions.size()];
            for (int i = 0; i < expressions.size(); i++) {
                pres[i] = builder.like(expressions.get(i), "%" + trim(keyword) + "%");
            }
            predicates.add(builder.or(pres));
        }
    }

    @SafeVarargs
    public final void addOrNotLikePredicate(String keyword, Expression<String>... exps) {
        if (null == exps || exps.length == 0 || StringUtils.isBlank(keyword)) return;
        ArrayList<Expression<String>> expressions = Lists.newArrayList(exps);

        if (expressions.size() == 1) {
            predicates.add(builder.notLike(expressions.get(0), "%" + trim(keyword) + "%"));
        } else {
            Predicate[] pres = new Predicate[expressions.size()];
            for (int i = 0; i < expressions.size(); i++) {
                pres[i] = builder.notLike(expressions.get(i), "%" + trim(keyword) + "%");
            }
            predicates.add(builder.or(pres));
        }
    }

    public void addOrLikePredicate(Expression<String> exp, List<?> keywords) {
        if (null == keywords || keywords.size() == 0 || null == exp) return;
        if (keywords.size() == 1) {
            predicates.add(builder.like(exp, "%" + trim(String.valueOf(keywords.get(0))) + "%"));
        } else {
            Predicate[] pres = new Predicate[keywords.size()];
            for (int i = 0; i < keywords.size(); i++) {
                pres[i] = builder.like(exp, "%" + trim(String.valueOf(keywords.get(i))) + "%");
            }
            predicates.add(builder.or(pres));
        }
    }

    public void addOrNotLikePredicate(Expression<String> exp, List<?> keywords) {
        if (null == keywords || keywords.size() == 0 || null == exp) return;
        if (keywords.size() == 1) {
            predicates.add(builder.notLike(exp, "%" + trim(String.valueOf(keywords.get(0))) + "%"));
        } else {
            Predicate[] pres = new Predicate[keywords.size()];
            for (int i = 0; i < keywords.size(); i++) {
                pres[i] = builder.notLike(exp, "%" + trim(String.valueOf(keywords.get(i))) + "%");
            }
            predicates.add(builder.or(pres));
        }
    }

    @SafeVarargs
    public final void addAndLikePredicate(String keyword, Expression<String>... exps) {
        if (null == exps || exps.length == 0 || StringUtils.isBlank(keyword)) return;
        ArrayList<Expression<String>> expressions = Lists.newArrayList(exps);

        if (expressions.size() == 1) {
            predicates.add(builder.like(expressions.get(0), "%" + trim(keyword) + "%"));
        } else {
            Predicate[] pres = new Predicate[expressions.size()];
            for (int i = 0; i < expressions.size(); i++) {
                pres[i] = builder.like(expressions.get(i), "%" + trim(keyword) + "%");
            }
            predicates.add(builder.and(pres));
        }
    }

    @SafeVarargs
    public final void addAndNotLikePredicate(String keyword, Expression<String>... exps) {
        if (null == exps || exps.length == 0 || StringUtils.isBlank(keyword)) return;
        ArrayList<Expression<String>> expressions = Lists.newArrayList(exps);

        if (expressions.size() == 1) {
            predicates.add(builder.notLike(expressions.get(0), "%" + trim(keyword) + "%"));
        } else {
            Predicate[] pres = new Predicate[expressions.size()];
            for (int i = 0; i < expressions.size(); i++) {
                pres[i] = builder.notLike(expressions.get(i), "%" + trim(keyword) + "%");
            }
            predicates.add(builder.and(pres));
        }
    }

    public void addAndLikePredicate(Expression<String> exp, List<?> keywords) {
        if (null == keywords || keywords.size() == 0 || null == exp) return;
        if (keywords.size() == 1) {
            predicates.add(builder.like(exp, "%" + trim(String.valueOf(keywords.get(0))) + "%"));
        } else {
            Predicate[] pres = new Predicate[keywords.size()];
            for (int i = 0; i < keywords.size(); i++) {
                pres[i] = builder.like(exp, "%" + trim(String.valueOf(keywords.get(i))) + "%");
            }
            predicates.add(builder.and(pres));
        }
    }

    public void addAndNotLikePredicate(Expression<String> exp, List<?> keywords) {
        if (null == keywords || keywords.size() == 0 || null == exp) return;
        if (keywords.size() == 1) {
            predicates.add(builder.notLike(exp, "%" + trim(String.valueOf(keywords.get(0))) + "%"));
        } else {
            Predicate[] pres = new Predicate[keywords.size()];
            for (int i = 0; i < keywords.size(); i++) {
                pres[i] = builder.notLike(exp, "%" + trim(String.valueOf(keywords.get(i))) + "%");
            }
            predicates.add(builder.and(pres));
        }
    }

    public void addIsTrue(Expression<Boolean> exp) {
        predicates.add(builder.isTrue(exp));
    }

    public void addIsFalse(Expression<Boolean> exp) {
        predicates.add(builder.isFalse(exp));
    }

    public void addIsNull(Expression<?> exp) {
        predicates.add(builder.isNull(exp));
    }

    public void addIsNotNull(Expression<?> exp) {
        predicates.add(builder.isNotNull(exp));
    }

    public void addJsonContainsPredicate(Expression<?> exp, String val) {
        predicates.add(builder.isTrue(builder.function("JSON_CONTAINS", Boolean.class, exp, builder.literal(val))));
    }

    public void addJsonNotContainsPredicate(Expression<?> exp, String val) {
        predicates.add(builder.isFalse(builder.function("JSON_CONTAINS", Boolean.class, exp, builder.literal(val))));
    }

    /**
     * 构建距离查询
     *
     * @param latitude    纬度字段
     * @param longitude   经度字段
     * @param coordinate  对比经纬度
     * @param minDistance 最小距离(米)
     * @param maxDistance 最大距离(米)
     * @param equals      距离是否相等
     * @param direction   距离排序方式
     */
    public void addDistanceBetween(Expression<?> latitude, Expression<?> longitude, GeoUtil.Coordinate coordinate, Number minDistance, Number maxDistance, boolean equals, Sort.Direction direction) {
        final Expression<Double> point = builder.function("point", Double.class, longitude, latitude);
        Expression<Double> point2 = builder.function("point", Double.class, builder.literal(coordinate.getLongitude()), builder.literal(coordinate.getLatitude()));

        Expression<Number> distance = builder.function("ST_Distance_Sphere", Number.class, point, point2);
        if (minDistance.doubleValue() > 0) {
            predicates.add(equals ? builder.ge(distance, minDistance) : builder.gt(distance, minDistance));
        }
        if (maxDistance.doubleValue() > 0) {
            predicates.add(equals ? builder.le(distance, maxDistance) : builder.lt(distance, maxDistance));
        }
        if (null != direction) {
            switch (direction) {
                case ASC:
                    addAscOrderBy(distance);
                    break;
                case DESC:
                    addDescOrderBy(distance);
                    break;
            }
        }
    }

    public void addDistanceBetween(Expression<?> latitude, Expression<?> longitude, GeoUtil.Coordinate coordinate, Number minDistance, Number maxDistance, boolean equals) {
        this.addDistanceBetween(latitude, longitude, coordinate, minDistance, maxDistance, equals, null);
    }

    public void addDistanceBetween(Expression<?> latitude, Expression<?> longitude, GeoUtil.Coordinate coordinate, Number minDistance, Number maxDistance, Sort.Direction direction) {
        this.addDistanceBetween(latitude, longitude, coordinate, minDistance, maxDistance, false, direction);
    }

    public void addDistanceLessThan(Expression<?> latitude, Expression<?> longitude, GeoUtil.Coordinate coordinate, Number maxDistance, boolean equals, Sort.Direction direction) {
        this.addDistanceBetween(latitude, longitude, coordinate, 0, maxDistance, equals, direction);
    }

    public void addDistanceLessThan(Expression<?> latitude, Expression<?> longitude, GeoUtil.Coordinate coordinate, Number maxDistance, boolean equals) {
        this.addDistanceBetween(latitude, longitude, coordinate, 0, maxDistance, equals);
    }

    public void addDistanceLessThan(Expression<?> latitude, Expression<?> longitude, GeoUtil.Coordinate coordinate, Number maxDistance, Sort.Direction direction) {
        this.addDistanceBetween(latitude, longitude, coordinate, 0, maxDistance, false, direction);
    }

    public void addDistanceGreaterThan(Expression<?> latitude, Expression<?> longitude, GeoUtil.Coordinate coordinate, Number minDistance, boolean equals, Sort.Direction direction) {
        this.addDistanceBetween(latitude, longitude, coordinate, minDistance, 0, equals, direction);
    }

    public void addDistanceGreaterThan(Expression<?> latitude, Expression<?> longitude, GeoUtil.Coordinate coordinate, Number minDistance, boolean equals) {
        this.addDistanceBetween(latitude, longitude, coordinate, minDistance, 0, equals);
    }

    public void addDistanceGreaterThan(Expression<?> latitude, Expression<?> longitude, GeoUtil.Coordinate coordinate, Number minDistance, Sort.Direction direction) {
        this.addDistanceBetween(latitude, longitude, coordinate, minDistance, 0, false, direction);
    }

    public void addDistanceOrderBy(Expression<?> latitude, Expression<?> longitude, GeoUtil.Coordinate coordinate, Sort.Direction direction) {
        this.addDistanceBetween(latitude, longitude, coordinate, 0, 0, false, direction);
    }

    public void addDescOrderBy(Expression<?> exp) {
        addOrderBy(exp, false);
    }

    public void addAscOrderBy(Expression<?> exp) {
        addOrderBy(exp, true);
    }

    public void addOrderBy(Expression<?> exp, boolean asc) {
        List<Order> orders = new ArrayList<>(query.getOrderList());
        orders.add(new OrderImpl(exp, asc));
        query.orderBy(orders);
    }

    protected <S> S getBean(Class<S> requiredType) {
        return SpringContextHolder.getBean(requiredType);
    }

    /**
     * 是否添加通用查询条件
     *
     * @return 是/否
     */
    public boolean addUniversalCondition() {
        return true;
    }

    /**
     * 是否要添加自定义的查询字段
     */
    public void generateCustomSelections(List<Selection<?>> selections, Root<T> root, CriteriaQuery<Tuple> query, CriteriaBuilder builder) {

    }
}
